﻿#region File and License Information
/*
<File>
	<License>
		Copyright © 2009 - 2017, Daniel Vaughan. All rights reserved.
		This file is part of Calcium (http://calciumsdk.net) 
		which is released under the MIT License.
		See file /Documentation/License.txt for full license details.
	</License>
	<CreationDate>2010-10-21 15:34:42Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Input;

using Outcoder.CommandModel;
using Outcoder.ComponentModel;

namespace Outcoder.UI.Xaml
{
	/// <summary>
	/// Interface specifying the functionality to be implemented 
	/// by an object that wishe to manage the handling of exceptions.
	/// </summary>
	public interface IExceptionHandler
	{
		bool HandleException(Exception exception, object owner, string memberName = null, string fileName = null, int lineNumber = 0);
	}

	public interface IDelegateCommand
	{
		void RaiseCanExecuteChanged();

		Task RefreshAsync(object commandParameter);
	}

	public class DelegateCommand<TParameter> : IEventCommand, IDelegateCommand, ISupportInitialize, INotifyPropertyChanged
	{
		IExceptionHandler exceptionHandler;
		volatile bool ignoreNextCanExecuteCall;

#if WINDOWS_UWP
		bool parameterPrimitive = typeof(TParameter).GetTypeInfo().IsPrimitive;
#else
		bool parameterPrimitive = typeof(TParameter).IsPrimitive;
#endif

#if WINDOWS_PHONE
		string eventName = "Tap";
#else
		string eventName = "Click";
#endif

		public string EventName
		{
			get
			{
				return eventName;
			}
			set
			{
				eventName = value;
			}
		}

		readonly Action<TParameter> executeAction;
		readonly Func<TParameter, bool> canExecuteFunc;
	
		readonly Func<TParameter, Task> executeActionAsync;
		Func<TParameter, Task<bool>> canExecuteAsyncFunc;

		string filePath;
		int lineNumber;

		/// <summary>
		/// Creates an awaitable asynchronous command. 
		/// </summary>
		/// <param name="executeAction">The action to execute when the command is performed.</param>
		/// <param name="canExecuteFunc">A func that determines if the command 
		/// may be performed. Can be <c>null</c>.</param>
		/// <param name="exceptionHandler">When an exception occurs during execution or during evaluating 
		/// if the command can execute, then the exception is passed to the exception manager. 
		/// Can be <c>null</c>.</param>
		/// <param name="filePath">The path to the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		/// <param name="lineNumber">The line number of the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		public DelegateCommand(Action<TParameter> executeAction, Func<TParameter, bool> canExecuteFunc = null,
			IExceptionHandler exceptionHandler = null, 
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
		{
			this.executeAction = ArgumentValidator.AssertNotNull(executeAction, "executeFunc");
			this.canExecuteFunc = canExecuteFunc;
			this.filePath = filePath;
			this.lineNumber = lineNumber;

			SetExceptionHandler(exceptionHandler);
		}

		/// <summary>
		/// Creates an awaitable asynchronous command. 
		/// </summary>
		/// <param name="executeFunc">The func to execute when the command is performed.</param>
		/// <param name="asyncConstructorDisambiguator">Can be any value and is used 
		/// to disambiguate between this and the synchronous constructor.</param>
		/// <param name="canExecuteFunc">A func that determines if the command 
		/// may be performed. Can be <c>null</c>.</param>
		/// <param name="exceptionHandler">When an exception occurs during execution or during evaluating 
		/// if the command can execute, then the exception is passed to the exception manager. 
		/// Can be <c>null</c>.</param>
		/// <param name="filePath">The path to the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		/// <param name="lineNumber">The line number of the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		public DelegateCommand(
			Func<TParameter, Task> executeFunc,
			bool asyncConstructorDisambiguator,
			Func<TParameter, Task<bool>> canExecuteFunc = null, 
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
		{
			executeActionAsync = ArgumentValidator.AssertNotNull(executeFunc, "executeFunc");

			canExecuteAsyncFunc = canExecuteFunc;
			isAsync = true;

			this.filePath = filePath;
			this.lineNumber = lineNumber;

			SetExceptionHandler(exceptionHandler);
		}

		bool isAsync;

		protected bool IsAsync => isAsync;

		void SetExceptionHandler(IExceptionHandler exceptionHandler)
		{
			if (exceptionHandler != null)
			{
				this.exceptionHandler = exceptionHandler;
			}
			else
			{
				if (Dependency.Initialized)
				{
					Dependency.TryResolve<IExceptionHandler>(out this.exceptionHandler);
				}
			}
		}

		protected bool HandleException(
			Exception exception, 
			object owner, 
			[CallerMemberName]string memberName = null)
		{
			if (exceptionHandler != null)
			{
				return exceptionHandler.HandleException(exception, owner, memberName);
			}

			return false;
		}

		public Func<TParameter, Task<bool>> CanExecuteFunc
		{
			get
			{
				return canExecuteAsyncFunc;
			}
			set
			{
				if (canExecuteAsyncFunc != value)
				{
					canExecuteAsyncFunc = value;

					isAsync = canExecuteFunc != null;

					OnPropertyChanged();

					OnCanExecuteChanged(EventArgs.Empty);
				}
			}
		}

#region ICommand Members

#if SILVERLIGHT || MONODROID || __IOS__ || NETFX_CORE
		public event EventHandler CanExecuteChanged;
#else
		public event EventHandler CanExecuteChanged
		{
			add
			{
				if (canExecuteFunc != null)
				{
					System.Windows.Input.CommandManager.RequerySuggested += value;
				}
			}
			remove
			{
				if (canExecuteFunc != null)
				{
					System.Windows.Input.CommandManager.RequerySuggested -= value;
				}
			}
		}
#endif

		protected TParameter ProcessParameter(object parameter)
		{
			TParameter result;
			if (parameter == null)
			{
				result = DefaultParameter;
			}
			else
			{
				result = (TParameter)CoerceParameterToType(parameter);

				if (!parameterPrimitive && EqualityComparer<TParameter>.Default.Equals(DefaultParameter, default(TParameter)))
				{
					DefaultParameter = result;
				}
			}

			return result;
		}

		protected TParameter ProcessParameter(TParameter parameter)
		{
			TParameter result;
			if (EqualityComparer<TParameter>.Default.Equals(parameter, default(TParameter)))
			{
				result = DefaultParameter;
			}
			else
			{
				result = parameter;

				if (!parameterPrimitive && EqualityComparer<TParameter>.Default.Equals(DefaultParameter, default(TParameter)))
				{
					DefaultParameter = parameter;
				}
			}
			
			return result;
		}

		public bool CanExecute(object parameter = null)
		{
			TParameter coercedParameter = ProcessParameter(parameter);
			
			bool result;

			if (!isAsync)
			{
				try
				{
					result = CanExecute(coercedParameter);
				}
				catch (Exception ex)
				{
					result = false;

					if (exceptionHandler == null
						|| !exceptionHandler.HandleException(ex, this, "CanExecute", filePath, lineNumber))
					{
						throw;
					}
				}
			}
			else
			{
				RefreshCanExecute(coercedParameter);

				result = Enabled;
			}

			return result;
		}

		async void RefreshCanExecute(TParameter parameter)
		{
			try
			{
				await RefreshCanExecuteAsync(parameter);
			}
			catch (Exception ex)
			{
				exceptionHandler?.HandleException(ex, this);
			}
		}

		async Task RefreshCanExecuteAsync(TParameter parameter)
		{
			/* This method is intended to be called by the UI thread 
			 * as a result of a UI update. */
			if (ignoreNextCanExecuteCall)
			{
				ignoreNextCanExecuteCall = false;
				return;
			}
			
			if (canExecuteAsyncFunc != null)
			{
				bool temp = Enabled;

				try
				{
					temp = await canExecuteAsyncFunc(parameter);
				}
				catch (Exception ex)
				{
					if (exceptionHandler == null
					    || !exceptionHandler.HandleException(ex, this, "canExecuteAsyncFunc", filePath, lineNumber))
					{
						throw;
					}
				}

				if (Enabled != temp)
				{
					ignoreNextCanExecuteCall = true;

					Enabled = temp;

					OnCanExecuteChanged(EventArgs.Empty);
				}
			}
		}

		public virtual async Task<bool> CanExecuteAsync(TParameter parameter)
		{
			parameter = ProcessParameter(parameter);

			bool result;

			if (!isAsync)
			{
				result = CanExecute(parameter);
			}
			else
			{
				await RefreshCanExecuteAsync(parameter);

				result = Enabled;
			}

			return result;
		}

		void ICommand.Execute(object parameter)
		{
			TParameter coercedParameter = ProcessParameter(parameter);
			Execute(coercedParameter);
		}

		public virtual async void Execute(TParameter parameter = default(TParameter))
		{
			await ExecuteAsync(parameter);
		}

		public virtual async Task ExecuteAsync(TParameter parameter = default(TParameter))
		{
			try
			{
				parameter = ProcessParameter(parameter);

				if (!isAsync)
				{
					executeAction(parameter);
				}
				else
				{
					await executeActionAsync(parameter);
				}
			}
			catch (Exception ex)
			{
				if (exceptionHandler == null
					|| !exceptionHandler.HandleException(ex, this, "ExecuteAsync", filePath, lineNumber))
				{
					throw;
				}
			}
		}

		object CoerceParameterToType(object parameter)
		{
			object coercedParameter = parameter;
			Type typeOfT = typeof(TParameter);

#if NETFX_CORE
			if (parameter != null && !typeOfT.GetTypeInfo().IsAssignableFrom(parameter.GetType().GetTypeInfo()))
#else
			if (parameter != null && !typeOfT.IsAssignableFrom(parameter.GetType()))
#endif
			{
				coercedParameter = ImplicitTypeConverter.ConvertToType(parameter, typeOfT);
			}
			return coercedParameter;
		}

#endregion

		public virtual bool CanExecute(TParameter parameter = default(TParameter))
		{
			parameter = ProcessParameter(parameter);

			bool result;

			if (!isAsync)
			{
				if (canExecuteFunc == null)
				{
					result = true;
				}
				else
				{
					try
					{
						result = canExecuteFunc(parameter);
					}
					catch (Exception ex)
					{
						result = true;

						if (exceptionHandler == null
							|| !exceptionHandler.HandleException(ex, this, "CanExecute", filePath, lineNumber))
						{
							throw;
						}
					}
				}
				
				Enabled = result;
			}
			else
			{
#pragma warning disable 4014
				RefreshCanExecuteAsync(parameter);
#pragma warning restore 4014

				result = Enabled;
			}

			return result;
		}

		bool enabled = true;

		public bool Enabled
		{
			get
			{
				return enabled;
			}
			protected internal set
			{
				AssignPropertyValue(ref enabled, value, false);
			}
		}

#if SILVERLIGHT || MONODROID || __IOS__ || NETFX_CORE
		public void RaiseCanExecuteChanged()
		{
			OnCanExecuteChanged(EventArgs.Empty);
		}

		protected virtual void OnCanExecuteChanged(EventArgs e)
		{
			var tempEvent = CanExecuteChanged;
			if (tempEvent != null)
			{
				UISynchronizationContext.Instance.InvokeIfRequired(() => tempEvent(this, e));
			}
		}
#else
		public void RaiseCanExecuteChanged()
		{
			OnCanExecuteChanged(EventArgs.Empty);	
		}

		protected virtual void OnCanExecuteChanged(EventArgs e)
		{
			/* This is present for cross-platform compatibility. */
		}
#endif

		public event PropertyChangedEventHandler PropertyChanged;

		protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
		{
			var temp = PropertyChanged;
			if (temp != null)
			{
				temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		protected bool Initializing { get; set; }

		public void BeginInit()
		{
			Initializing = true;
		}

		public void EndInit()
		{
			Initializing = false;
		}

		public virtual async Task RefreshAsync(object commandParameter = null)
		{
			TParameter parameter = ProcessParameter(commandParameter);
			//var argOfT = ArgumentValidator.AssertOfType<TParameter>(commandParameter, "commandParameter");
#pragma warning disable 4014
			await RefreshCanExecuteAsync(parameter);
#pragma warning restore 4014

		}

		public virtual async Task RefreshAsync(TParameter parameter)
		{
			parameter = ProcessParameter(parameter);

			if (isAsync)
			{
				await RefreshCanExecuteAsync(parameter);
			}
			else
			{
				bool canCurrentlyExecute = Enabled;
				bool valueAfterUpdate = CanExecute(parameter);
				if (canCurrentlyExecute != valueAfterUpdate)
				{
					OnCanExecuteChanged(EventArgs.Empty);
				}
			}
		}

		protected void AssignPropertyValue<TField>(
			ref TField oldValue, 
			TField newValue, 
			bool raiseOnCanExecuteChanged,
			[CallerMemberName] string memberName = null)
		{
			if (!object.Equals(oldValue, newValue))
			{
				oldValue = newValue;

				if (!Initializing)
				{
					// ReSharper disable once ExplicitCallerInfoArgument
					OnPropertyChanged(memberName);

					if (raiseOnCanExecuteChanged)
					{
						OnCanExecuteChanged(EventArgs.Empty);
					}
				}
			}
		}

		public TParameter DefaultParameter { get; set; }
	}

	public class DelegateCommand : DelegateCommand<object>
	{
		/// <summary>
		/// Creates an awaitable asynchronous command. 
		/// </summary>
		/// <param name="executeAction">The action to execute when the command is performed.</param>
		/// <param name="canExecute">An action that determines if the command 
		/// may be performed. Can be <c>null</c>.</param>
		/// <param name="exceptionHandler">When an exception occurs during execution or during evaluating 
		/// if the command can execute, then the exception is passed to the exception manager. 
		/// Can be <c>null</c>.</param>
		/// <param name="filePath">The path to the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		/// <param name="lineNumber">The line number of the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		public DelegateCommand(
			Action<object> executeAction, 
			Func<object, bool> canExecute = null,
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
			: base(executeAction, canExecute, 
					exceptionHandler, filePath, lineNumber)
		{
			/* Intentionally left blank. */
		}

		/// <summary>
		/// Creates an awaitable asynchronous command. 
		/// </summary>
		/// <param name="executeFunc">The action to execute when the command is performed.</param>
		/// <param name="asyncConstructorDisambiguator">Can be any value and is used 
		/// to disambiguate between this and the synchronous constructor.</param>
		/// <param name="canExecuteFunc">A func that determines if the command 
		/// may be performed. Can be <c>null</c>.</param>
		/// <param name="exceptionHandler">When an exception occurs during execution or during evaluating 
		/// if the command can execute, then the exception is passed to the exception manager. 
		/// Can be <c>null</c>.</param>
		/// <param name="filePath">The path to the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		/// <param name="lineNumber">The line number of the file that is instantiating this class. 
		/// This should only be explicitly specified  by classes that subclass this class.</param>
		public DelegateCommand(
			Func<object, Task> executeFunc,
			bool asyncConstructorDisambiguator,
			Func<object, Task<bool>> canExecuteFunc = null,
			IExceptionHandler exceptionHandler = null,
			[CallerFilePath] string filePath = null,
			[CallerLineNumber] int lineNumber = 0)
			: base(executeFunc, asyncConstructorDisambiguator, 
				canExecuteFunc, exceptionHandler, filePath, lineNumber)
		{
			/* Intentionally left blank. */
		}
	}
}
